package net.cloudcodex.server.service;


import static net.cloudcodex.shared.Errors.IMPOSSIBLE_EXISTS;
import static net.cloudcodex.shared.Errors.NOT_FOUND_CAMPAIGN;
import static net.cloudcodex.shared.Errors.NOT_FOUND_CHARACTER;
import static net.cloudcodex.shared.Errors.NOT_FOUND_USER;
import static net.cloudcodex.shared.Errors.REQUIRED;
import static net.cloudcodex.shared.Errors.USER_USURPATION_GM;
import static net.cloudcodex.shared.Errors.USER_USURPATION_NPC;
import static net.cloudcodex.shared.Errors.USER_USURPATION_PC;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import net.cloudcodex.server.Context;
import net.cloudcodex.server.data.Data;
import net.cloudcodex.server.data.Data.Campaign;
import net.cloudcodex.server.data.Data.CharacterNote;
import net.cloudcodex.server.data.Data.User;
import net.cloudcodex.server.data.campaign.character.CharacterDescriptionSDO;
import net.cloudcodex.server.data.campaign.character.CharacterSDO;
import net.cloudcodex.shared.Errors;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Query.FilterOperator;
import com.google.appengine.api.datastore.Transaction;

/**
 * Service for Campaign
 * @author Thomas
 */
public class CampaignService extends AbstractCampaignService {

	/**
	 * @param store Google AppEngine datastore.
	 */
	public CampaignService(DatastoreService store) {
		super(store);
	}
	
	/**
	 * Creates a campaign.
	 * 
	 * @param name name of the campaign to create.
	 * @param master game master
	 * @return the newly created campaign object.
	 */
	public Data.Campaign createCampaign(Context context, String name, 
			String game, String description, String icon,
			Data.User master) {
		
		if(master == null || name == null) {
			logger.severe("missing param");
			context.addError(REQUIRED);
			return null;
		}
		
		// check the campaign doesn't already exist
		Campaign campaign = dao.getCampaignByName(context, name);
		if(campaign != null) {
			logger.severe("campaign '" + name + "' alreayd exists");
			context.addError(IMPOSSIBLE_EXISTS, name);
			return null;
		}
		
		campaign = new Campaign();
		campaign.setName(name);
		campaign.setGame(game);
		campaign.setMaster(master);
		campaign.setDate(new Date());
		campaign.setDescription(description);
		campaign.setIcon(icon);
		campaign.setDate(new Date());
		dao.save(context, campaign);

		return campaign;
	}
	
	/**
	 * Updates a campaign.
	 * 
	 * @param campaignId id of the campaign to update.
	 * @param name name of the campaign
	 * @param master game master
	 * @return the newly created campaign object.
	 */
	public Data.Campaign updateCampaign(Context context, long campaignId,
			String name, String game, String description, String icon,
			Data.User master) {
		
		if(master == null || name == null) {
			logger.severe("missing param");
			context.addError(REQUIRED);
			return null;
		}
		
		// Check campaign exists.
		final Data.Campaign campaign = dao.readCampaign(context, campaignId);
		
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignId);
			context.addError(NOT_FOUND_CAMPAIGN, campaignId);
			return null;
		}
		
		// Check current user is campaign GM
		final Data.User user = context.getUser();
		if(!isGameMaster(context, campaign)) {
			logger.severe("Unauthorized access by " + user.getKey() + " on " + campaignId);
			context.addError(USER_USURPATION_GM);
			return null;
		}
		
		// check the campaign doesn't already exist
		Campaign otherCampaign = dao.getCampaignByName(context, name);
		if(otherCampaign != null) {
			logger.severe("campaign '" + name + "' alreayd exists");
			context.addError(IMPOSSIBLE_EXISTS, name);
			return null;
		}
		
		campaign.setName(name);
		campaign.setGame(game);
		campaign.setDescription(description);
		campaign.setIcon(icon);
		
		dao.save(context, campaign);

		return campaign;
	}
	

	public Data.Character createCharacter(Context context, Campaign campaign, String name, 
			User owner, String icon, String description) {
		
		return createCharacter(context, campaign, name, owner, icon, description, false);
	}

	/**
	 * Creates a character if its doesn't already exists.
	 * 
	 * @param campaign campaign of the character.
	 * @param name name of the character.
	 * @param owner owner (user) of the character.
	 * @return the newly created character.
	 */
	public Data.Character createCharacter(Context context, Campaign campaign, String name, 
			User owner, String icon, String description, boolean profile) {
		
		if(campaign == null || name == null) {
			logger.severe("missing param");
			context.addError(REQUIRED);
			return null;
		}
		
		name = name.trim();
		
		// check the character doesn't already exist
		Data.Character character = 
				dao.getCharacterByName(context, campaign.getKey(), name);
		if(character != null) {
			logger.severe("character '" + name 
					+ "' alreayd exists in " + campaign.getKey());
			context.addError(IMPOSSIBLE_EXISTS, name, campaign.getKey());
			return null;
		}
		
		character = new Data.Character(campaign);
		character.setName(name);
		character.setOwner(owner);
		character.setOwnerNickname(owner == null ? null : owner.getNickname());
		character.setDate(new Date());
		character.setIcon(icon);
		character.setDescription(description);
		character.setProfile(profile);
		dao.save(context, character);
		return character;
	}

	/**
	 * Add a user to a campaign.
	 */
	public CharacterSDO inviteToCampaign(Context context, long campaignId, String email, String name) {
		
		if(email == null || name == null) {
			logger.severe("missing param");
			context.addError(REQUIRED);
			return null;
		}
		
		// Check campaign exists.
		final Data.Campaign campaign = dao.readCampaign(context, campaignId);
		
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignId);
			context.addError(NOT_FOUND_CAMPAIGN, campaignId);
			return null;
		}

		// Check current user is campaign GM
		final Data.User user = context.getUser();
		if(!isGameMaster(context, campaign)) {
			logger.severe("Unauthorized access by " + user.getKey() + " on " + campaignId);
			context.addError(USER_USURPATION_GM);
			return null;
		}

		// Check the user to add
		final Data.User player = dao.getUserByEmail(context, email);
		if(player == null) {
			logger.severe("User not found " + email);
			context.addError(NOT_FOUND_USER, email);
			return null;
		}
		
		// Check the GM is not inviting itself
		if(StringUtils.equals(player.getEmail(), context.getUser().getEmail())) {
			logger.severe("GM is inviting itself");
			context.addError(Errors.IMPOSSIBLE_YOURSELF);
			return null;
		}
		
		// Create the character.
		final Data.Character character = 
			createCharacter(context, campaign, name, player, null, null);

		if(character == null) {
			return null;
		}
		
		final CharacterSDO sdo = new CharacterSDO();
		sdo.setCampaign(campaign);
		sdo.setCharacter(character);
		
		return sdo;
	}
	
	/**
	 * Creates a NPC.
	 * @param context execution context.
	 * @param campaignId campaign's id.
	 * @param name NPC's name, cannot be null.
	 * @param icon NPC's icon.
	 * @param description NPC's description.
	 * @param <code>true</code> for profiles.
	 * @return the newly created NPC.
	 */
	public CharacterSDO createNPC(Context context, long campaignId, 
			String name, String icon, String description, boolean profile) {
		
		if(name == null) {
			logger.severe("no name specified");
			context.addError(REQUIRED, "name");
			return null;
		}
		
		// Check campaign exists.
		final Data.Campaign campaign = dao.readCampaign(context, campaignId);
		
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignId);
			context.addError(NOT_FOUND_CAMPAIGN, campaignId);
			return null;
		}

		// Check current user is campaign GM
		final Data.User user = context.getUser();
		if(!campaign.getMaster().equals(user.getKey())) {
			logger.severe("Unauthorized access by " 
					+ user.getKey() + " on " + campaignId);
			context.addError(USER_USURPATION_GM);
			return null;
		}

		// Create the character.
		final Data.Character character = 
			createCharacter(context, campaign, name, null, icon, description, profile);

		if(character == null) {
			return null;
		}
		
		final CharacterSDO sdo = new CharacterSDO();
		sdo.setCampaign(campaign);
		sdo.setCharacter(character);
		
		return sdo;
	}
	
	/**
	 * Returns the campaign's characters, only GM cans call this method.
	 * @param context execution context.
	 * @param campaignId campaign id.
	 * @return the campaign's characters.
	 */
	public List<CharacterSDO> getCampaignCharacters(Context context, long campaignId) {
		
		final Campaign campaign = dao.readCampaign(context, campaignId);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignId);
			context.addError(NOT_FOUND_CAMPAIGN, campaignId);
			return null;
		}

		final Key campaignKey = campaign.getKey();
		
		// Check current user is campaign GM
		final Data.User user = context.getUser();
		if(!campaign.getMaster().equals(user.getKey())) {
			logger.severe("Unauthorized access by " 
					+ user.getKey() + " on " + campaignKey);
			context.addError(USER_USURPATION_GM);
			return null;
		}

		// Get all characters
		final List<Data.Character> characters = dao.asListOfCharacters(
				context, dao.queryCharacter(campaignKey), null);
		
		if(characters == null || characters.isEmpty()) {
			return null;
		}
		
		// Aggregate all data.
		final List<CharacterSDO> sdos = new ArrayList<CharacterSDO>();
		for(Data.Character character : characters) {
			final CharacterSDO sdo = new CharacterSDO();
			sdo.setCampaign(campaign);
			sdo.setCharacter(character);
			sdos.add(sdo);
		}
		
		return sdos.isEmpty() ? null : sdos;
	}
	
	/**
	 * Consult the current user character header.
	 * 
	 * @param context execution context.
	 * @param campaignId campaign's id.
	 * @param characterId character's id.
	 * @return the character's header.
	 */
	public CharacterSDO getCharacter(Context context, long campaignId, long characterId) {
		
		final Campaign campaign = dao.readCampaign(context, campaignId);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignId);
			context.addError(NOT_FOUND_CAMPAIGN, campaignId);
			return null;
		}

		final Key campaignKey = campaign.getKey();
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		
		// Check the character exists
		final Data.Character character = dao.readCharacter(context, characterKey);
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			context.addError(NOT_FOUND_CHARACTER, characterId);
			return null;
		}

		// check its not a NPC
		if(character.getOwner() == null) {
			logger.severe("User " + context.getUser().getKey()
					+ " tried to get character description as NPC");
			context.addError(USER_USURPATION_NPC);
			return null;
		}
		
		// check user is its owner.
		if(!isOwner(context, character) && !isGameMaster(context, campaign)) {
			logger.severe("User " + context.getUser().getKey()
					+ " tried to get character description as " 
					+ characterKey);
			context.addError(USER_USURPATION_PC);
			return null;
		}
		
		final CharacterSDO sdo = new CharacterSDO();
		sdo.setCampaign(campaign);
		sdo.setCharacter(character);
		
		return sdo;
	}
	
	
	
	/**
	 * @param context execution context.
	 * @param campaignId camapign's id.
	 * @param characterId character's id.
	 * @param timestamp last character description timestamp, may be null.
	 * @return the character description, or just the changes if timestamp is not null.
	 */
	public CharacterDescriptionSDO getCharacterDescription(
		Context context, long campaignId, long characterId, 
		Long byCharacterId, Date timestamp) {

		final Key campaignKey = Campaign.createKey(campaignId);
		final Data.Campaign campaign = dao.readCampaign(context, campaignKey);
		if(campaign == null) {
			logger.severe("Invalid campaign " + campaignKey);
			context.addError(NOT_FOUND_CAMPAIGN, campaignId);
			return null;
		}

		final Key characterKey = Data.Character.createKey(campaignKey, characterId);

		// Check the user is GM
		final boolean master = isGameMaster(context, campaign);
		
		if(!master && byCharacterId == null) {
			logger.severe("User " + context.getUser().getKey()
					+ " tried to get character description as GM");
			context.addError(USER_USURPATION_GM);
			return null;
		}
		
		// Done before reading to avoid a concurrent insert never read after.
		final Date readTimestamp = new Date();
		
		final Query query;
		if(timestamp != null) {
			// Get notes with timestamp field > timestamp param
			query = dao.addCharacterNoteFilterOnTimestamp(
							dao.queryCharacterNote(characterKey), 
							FilterOperator.GREATER_THAN, timestamp);
		} else {
			query = dao.queryCharacterNote(characterKey);
		}
		
		final CharacterDescriptionSDO description = new CharacterDescriptionSDO();
		description.setTimestamp(readTimestamp);
		description.setCampaignId(campaignId);
		description.setCharacterId(characterId);

		// Get the character entity to test if its a profile or not.
		final Data.Character character = dao.readCharacter(context, characterKey);
		if(character == null) {
			logger.severe("Invalid character " + characterKey);
			context.addError(NOT_FOUND_CHARACTER, characterId);
			return null;
		}

		if(!Boolean.TRUE.equals(character.getProfile())) {
			if(master) {
				// GM sees all notes
				description.setNotes(dao.asListOfCharacterNotes(context, query, null));
				
				// GMM sees the character's sheet.
				description.setSheet(character.getSheet());

			} else {
				// check the character viewing exists
				final Key byCharacterKey = Data.Character.createKey(campaignKey, byCharacterId);
				final Data.Character byCharacter = dao.readCharacter(context, byCharacterKey);
				if(byCharacter == null) {
					logger.severe("Invalid character " + byCharacterKey);
					context.addError(NOT_FOUND_CHARACTER, byCharacterId);
					return null;
				}

				// check its not a NPC
				if(byCharacter.getOwner() == null) {
					logger.severe("User " + context.getUser().getKey()
							+ " tried to get character description as NPC");
					context.addError(USER_USURPATION_NPC);
					return null;
				}

				// check the user is its owner
				if(!isOwner(context, byCharacter)) {
					logger.severe("User " + context.getUser().getKey()
							+ " tried to get character description as " 
							+ byCharacterKey);
					context.addError(USER_USURPATION_PC);
					return null;
				}
				
				// Itself and others players can only see their own.
				description.setNotes(
						dao.asListOfCharacterNotes(context, 
								dao.addCharacterNoteFilterOnAuthor(
								query, FilterOperator.EQUAL, byCharacterKey), null));
				
				// the character consults itself
				if(byCharacterId != null && characterId == byCharacterId) {
					description.setSheet(byCharacter.getSheet());
				}
			}
		}

		if(master) {
			// map the character aliases.
			final Map<String, String> aliases = character.getAlias();
			if(aliases != null) {
				final Map<Long, String> descAliases = new HashMap<Long, String>();
				for(Map.Entry<String, String> entry : aliases.entrySet()) {
					try {
						final Long charId = Long.valueOf(entry.getKey());
						descAliases.put(charId, entry.getValue());
					} catch(NumberFormatException e) {
						logger.severe(entry.getKey() 
							+ " is not a valid charId for alias in " + character);
					}
				}
				if(descAliases != null) {
					description.setAliases(descAliases);
				}
			}
		}
		
		return description;
	}

	/**
	 * Update a character description and notes about.
	 * 
	 * @param context execution context.
	 * @param campaignId campaign's id.
	 * @param characterId description's character's id.
	 * @param byCharacterId id of the character updating the description, 
	 * 		<code>null</code> for game master.
	 * @param description description to update, null to not update.
	 * @param notes map author/note to update.
	 * @param notes map PC/alias to update, only for GM.
	 * 
	 * @return the result of {@link #getCharacterDescription(
	 * 		Context, long, long, Date)} after the update.
	 */
	public CharacterDescriptionSDO updateCharacterDescription(
		Context context, long campaignId, long characterId, Long byCharacterId,
		String description, String sheet, Boolean dead, Boolean locked,
		Map<Long, String> notes, Map<Long, String> aliases) {

		// Check the campaign and the master
		final Key campaignKey = Data.Campaign.createKey(campaignId);
		final Campaign campaign = dao.readCampaign(context, campaignKey);
		if(campaign == null) {
			logger.severe("Campaign not found " + campaignKey);
			context.addError(NOT_FOUND_CAMPAIGN, campaignId);
			return null;
		}

		// see if the user is game master
		final boolean master = isGameMaster(context, campaign);
		
		// Check the user to update exists
		final Key characterKey = Data.Character.createKey(campaignKey, characterId);
		final Data.Character character = dao.readCharacter(context, characterKey);
		if(character == null) {
			logger.severe("character not found " + characterKey);
			context.addError(NOT_FOUND_CHARACTER, characterId);
			return null;
		}
		
		if(Boolean.TRUE.equals(character.getProfile()) 
				&& notes != null && !notes.isEmpty()) {
			logger.severe("Character " + characterKey + " is a profile, "
					+ context.getUser().getKey() + " cannot make notes");
			context.addError(Errors.IMPOSSIBLE_PROFILE);
			return null;
		}
		
		final boolean npc = character.getOwner() == null;
		
		// Do the updates in a single transaction
		final Transaction tx = dao.beginTransaction();
		try {
			if(master) {
				// Update the character's description.
				if(description != null) {
					if("".equals(description.trim())) {
						character.setDescription(null);
					} else {
						character.setDescription(description);
					}
				}
				
				// Update the character's sheet.
				if(sheet != null) {
					if("".equals(sheet.trim())) {
						character.setSheet(null);
					} else {
						character.setSheet(sheet);
					}
				}

				// Update the character's aliases
				if(aliases != null) {
					for(Map.Entry<Long, String> entry : aliases.entrySet()) {
						if(entry.getKey() != null) {
							character.setAlias(entry.getKey().toString(), entry.getValue());
						}
					}
				}
				
				// master can update all notes. (but profile can't have notes)
				if(notes != null) {
					for(Map.Entry<Long, String> entry : notes.entrySet()) {

						// author's key is null for GM
						final Key authorKey = entry.getKey() == null ? null 
								: Data.Character.createKey(campaignKey, entry.getKey());

						// read the current note from this author
						CharacterNote noteDB = dao.getCharacterNoteByAuthor(context, characterKey, authorKey);
						if(noteDB == null) {
							noteDB = new CharacterNote(character);
							noteDB.setAuthor(authorKey);
						}
						
						// Notes are never deleted, just set to null (usefull when checking deltas)
						noteDB.setContent(entry.getValue());
						dao.save(context, noteDB);
					}
				}
				
				if(dead != null) {
					character.setDead(dead);
				}

				// Only PCs can be locked
				if(locked != null && !npc) {
					character.setLocked(locked);
				}
				
				dao.save(context, character);

			} else {
				
				if(notes != null) {
					if(byCharacterId == null) {
						logger.severe("user " + context.getUser().getKey()
								+ " has tried to update GM note on " + characterKey);
						context.addError(USER_USURPATION_GM);
						return null;
					}
					
					// players can only update their own.
					final Key authorKey = Data.Character.createKey(campaignKey, byCharacterId);

					// check the writer exists
					final Data.Character author = dao.readCharacter(context, authorKey);
					if(author == null) {
						logger.severe("character not found " + characterKey);
						context.addError(NOT_FOUND_CHARACTER, characterId);
						return null;
					}
					
					// check the user is the writer's owner
					if(!isOwner(context, author)) {
						logger.severe("user " + context.getUser().getKey() + " has tried to update " 
								+ author.getOwner() + "'s note on " + characterKey);
						context.addError(Errors.USER_USURPATION);
						return null;
					}

					// update or create the note if not an NPC
					CharacterNote noteDB = dao.getCharacterNoteByAuthor(context, characterKey, authorKey);
					if(noteDB == null) {
						noteDB = new CharacterNote(character);
						noteDB.setAuthor(authorKey);
					}
					noteDB.setContent(notes.get(byCharacterId));
					dao.save(context, noteDB);
				}
			}

			tx.commit();
			
		} finally {
			if(tx.isActive()) {
				tx.rollback();
			}
		}
		
		return getCharacterDescription(
				context, campaignId, characterId, byCharacterId, null);
	}
	

	
	
	
	

	
	
	
	
	
	
}
